/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.catalina.startup;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import javax.servlet.MultipartConfigElement;
import javax.servlet.ServletContainerInitializer;
import javax.servlet.ServletContext;
import javax.servlet.SessionCookieConfig;
import javax.servlet.annotation.HandlesTypes;

import org.apache.catalina.Authenticator;
import org.apache.catalina.Container;
import org.apache.catalina.Context;
import org.apache.catalina.Engine;
import org.apache.catalina.Globals;
import org.apache.catalina.Host;
import org.apache.catalina.Lifecycle;
import org.apache.catalina.LifecycleEvent;
import org.apache.catalina.LifecycleListener;
import org.apache.catalina.Pipeline;
import org.apache.catalina.Server;
import org.apache.catalina.Service;
import org.apache.catalina.Valve;
import org.apache.catalina.WebResource;
import org.apache.catalina.WebResourceRoot;
import org.apache.catalina.Wrapper;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.core.StandardHost;
import org.apache.catalina.util.ContextName;
import org.apache.catalina.util.Introspection;
import org.apache.jasper.servlet.JasperInitializer;
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
import org.apache.tomcat.Jar;
import org.apache.tomcat.JarScanType;
import org.apache.tomcat.JarScanner;
import org.apache.tomcat.util.ExceptionUtils;
import org.apache.tomcat.util.bcel.classfile.AnnotationElementValue;
import org.apache.tomcat.util.bcel.classfile.AnnotationEntry;
import org.apache.tomcat.util.bcel.classfile.ArrayElementValue;
import org.apache.tomcat.util.bcel.classfile.ClassFormatException;
import org.apache.tomcat.util.bcel.classfile.ClassParser;
import org.apache.tomcat.util.bcel.classfile.ElementValue;
import org.apache.tomcat.util.bcel.classfile.ElementValuePair;
import org.apache.tomcat.util.bcel.classfile.JavaClass;
import org.apache.tomcat.util.buf.UriUtil;
import org.apache.tomcat.util.descriptor.InputSourceUtil;
import org.apache.tomcat.util.descriptor.XmlErrorHandler;
import org.apache.tomcat.util.descriptor.web.ContextEjb;
import org.apache.tomcat.util.descriptor.web.ContextEnvironment;
import org.apache.tomcat.util.descriptor.web.ContextLocalEjb;
import org.apache.tomcat.util.descriptor.web.ContextResource;
import org.apache.tomcat.util.descriptor.web.ContextResourceEnvRef;
import org.apache.tomcat.util.descriptor.web.ContextService;
import org.apache.tomcat.util.descriptor.web.ErrorPage;
import org.apache.tomcat.util.descriptor.web.FilterDef;
import org.apache.tomcat.util.descriptor.web.FilterMap;
import org.apache.tomcat.util.descriptor.web.FragmentJarScannerCallback;
import org.apache.tomcat.util.descriptor.web.JspPropertyGroup;
import org.apache.tomcat.util.descriptor.web.LoginConfig;
import org.apache.tomcat.util.descriptor.web.MessageDestinationRef;
import org.apache.tomcat.util.descriptor.web.MultipartDef;
import org.apache.tomcat.util.descriptor.web.SecurityConstraint;
import org.apache.tomcat.util.descriptor.web.SecurityRoleRef;
import org.apache.tomcat.util.descriptor.web.ServletDef;
import org.apache.tomcat.util.descriptor.web.SessionConfig;
import org.apache.tomcat.util.descriptor.web.WebXml;
import org.apache.tomcat.util.descriptor.web.WebXmlParser;
import org.apache.tomcat.util.digester.Digester;
import org.apache.tomcat.util.digester.RuleSet;
import org.apache.tomcat.util.res.StringManager;
import org.apache.tomcat.util.scan.JarFactory;
import org.xml.sax.InputSource;
import org.xml.sax.SAXParseException;

/**
 * Startup event listener for a <b>Context</b> that configures the properties of that Context, and
 * the associated defined servlets.
 *
 * @author Craig R. McClanahan
 */
public class ContextConfig implements LifecycleListener {

  private static final Log log = LogFactory.getLog(ContextConfig.class);


  /**
   * The string resources for this package.
   */
  protected static final StringManager sm =
      StringManager.getManager(Constants.Package);


  protected static final LoginConfig DUMMY_LOGIN_CONFIG =
      new LoginConfig("NONE", null, null, null);


  /**
   * The set of Authenticators that we know how to configure.  The key is the name of the
   * implemented authentication method, and the value is the fully qualified Java class name of the
   * corresponding Valve.
   */
  protected static final Properties authenticators;

  static {
    // Load our mapping properties for the standard authenticators
    Properties props = new Properties();
    try (InputStream is = ContextConfig.class.getClassLoader().getResourceAsStream(
        "org/apache/catalina/startup/Authenticators.properties")) {
      if (is != null) {
        props.load(is);
      }
    } catch (IOException ioe) {
      props = null;
    }
    authenticators = props;
  }

  /**
   * Deployment count.
   */
  protected static long deploymentCount = 0L;


  /**
   * Cache of default web.xml fragments per Host
   */
  protected static final Map<Host, DefaultWebXmlCacheEntry> hostWebXmlCache =
      new ConcurrentHashMap<>();


  /**
   * Set used as the value for {@code JavaClassCacheEntry.sciSet} when there are no SCIs associated
   * with a class.
   */
  private static final Set<ServletContainerInitializer> EMPTY_SCI_SET = Collections.emptySet();

  // ----------------------------------------------------- Instance Variables
  /**
   * Custom mappings of login methods to authenticators
   */
  protected Map<String, Authenticator> customAuthenticators;


  /**
   * The Context we are associated with.
   */
  protected volatile Context context = null;


  /**
   * The default web application's deployment descriptor location.
   */
  protected String defaultWebXml = null;


  /**
   * Track any fatal errors during startup configuration processing.
   */
  protected boolean ok = false;


  /**
   * Original docBase.
   */
  protected String originalDocBase = null;


  /**
   * Anti-locking docBase. It is a path to a copy of the web application in the java.io.tmpdir
   * directory. This path is always an absolute one.
   */
  private File antiLockingDocBase = null;


  /**
   * Map of ServletContainerInitializer to classes they expressed interest in.
   */
  protected final Map<ServletContainerInitializer, Set<Class<?>>> initializerClassMap =
      new LinkedHashMap<>();

  /**
   * Map of Types to ServletContainerInitializer that are interested in those types.
   */
  protected final Map<Class<?>, Set<ServletContainerInitializer>> typeInitializerMap =
      new HashMap<>();

  /**
   * Flag that indicates if at least one {@link HandlesTypes} entry is present that represents an
   * annotation.
   */
  protected boolean handlesTypesAnnotations = false;

  /**
   * Flag that indicates if at least one {@link HandlesTypes} entry is present that represents a
   * non-annotation.
   */
  protected boolean handlesTypesNonAnnotations = false;

  // ------------------------------------------------------------- Properties

  /**
   * Obtain the location of the default deployment descriptor.
   *
   * @return The path to the default web.xml. If not absolute, it is relative to CATALINA_BASE.
   */
  public String getDefaultWebXml() {
    if (defaultWebXml == null) {
      defaultWebXml = Constants.DefaultWebXml;
    }
    return defaultWebXml;
  }


  /**
   * Set the location of the default deployment descriptor.
   *
   * @param path The path to the default web.xml. If not absolute, it is relative to CATALINA_BASE.
   */
  public void setDefaultWebXml(String path) {
    this.defaultWebXml = path;
  }


  /**
   * Sets custom mappings of login methods to authenticators.
   *
   * @param customAuthenticators Custom mappings of login methods to authenticators
   */
  public void setCustomAuthenticators(
      Map<String, Authenticator> customAuthenticators) {
    this.customAuthenticators = customAuthenticators;
  }

  // --------------------------------------------------------- Public Methods


  /**
   * Process events for an associated Context.
   *
   * @param event The lifecycle event that has occurred
   */
  @Override
  public void lifecycleEvent(LifecycleEvent event) {

    // Identify the context we are associated with
    try {
      context = (Context) event.getLifecycle();
    } catch (ClassCastException e) {
      log.error(sm.getString("contextConfig.cce", event.getLifecycle()), e);
      return;
    }

    // Process the event that has occurred
    if (event.getType().equals(Lifecycle.CONFIGURE_START_EVENT)) {
      configureStart();
    } else if (event.getType().equals(Lifecycle.BEFORE_START_EVENT)) {
      beforeStart();
    } else if (event.getType().equals(Lifecycle.AFTER_START_EVENT)) {
      // Restore docBase for management tools
      if (originalDocBase != null) {
        context.setDocBase(originalDocBase);
      }
    } else if (event.getType().equals(Lifecycle.CONFIGURE_STOP_EVENT)) {
      configureStop();
    } else if (event.getType().equals(Lifecycle.AFTER_INIT_EVENT)) {
      init();
    } else if (event.getType().equals(Lifecycle.AFTER_DESTROY_EVENT)) {
      destroy();
    }

  }

  // -------------------------------------------------------- protected Methods


  /**
   * Process the application classes annotations, if it exists.
   */
  protected void applicationAnnotationsConfig() {

    long t1 = System.currentTimeMillis();

    WebAnnotationSet.loadApplicationAnnotations(context);

    long t2 = System.currentTimeMillis();
    if (context instanceof StandardContext) {
      ((StandardContext) context).setStartupTime(t2 - t1 +
          ((StandardContext) context).getStartupTime());
    }
  }


  /**
   * Set up an Authenticator automatically if required, and one has not already been configured.
   */
  protected void authenticatorConfig() {

    LoginConfig loginConfig = context.getLoginConfig();
    if (loginConfig == null) {
      // Need an authenticator to support HttpServletRequest.login()
      loginConfig = DUMMY_LOGIN_CONFIG;
      context.setLoginConfig(loginConfig);
    }

    // Has an authenticator been configured already?
    if (context.getAuthenticator() != null) {
      return;
    }

    // Has a Realm been configured for us to authenticate against?
    if (context.getRealm() == null) {
      log.error(sm.getString("contextConfig.missingRealm"));
      ok = false;
      return;
    }

    /*
     * First check to see if there is a custom mapping for the login
     * method. If so, use it. Otherwise, check if there is a mapping in
     * org/apache/catalina/startup/Authenticators.properties.
     */
    Valve authenticator = null;
    if (customAuthenticators != null) {
      authenticator = (Valve) customAuthenticators.get(loginConfig.getAuthMethod());
    }

    if (authenticator == null) {
      if (authenticators == null) {
        log.error(sm.getString("contextConfig.authenticatorResources"));
        ok = false;
        return;
      }

      // Identify the class name of the Valve we should configure
      String authenticatorName = authenticators.getProperty(loginConfig.getAuthMethod());
      if (authenticatorName == null) {
        log.error(sm.getString("contextConfig.authenticatorMissing",
            loginConfig.getAuthMethod()));
        ok = false;
        return;
      }

      // Instantiate and install an Authenticator of the requested class
      try {
        Class<?> authenticatorClass = Class.forName(authenticatorName);
        authenticator = (Valve) authenticatorClass.getConstructor().newInstance();
      } catch (Throwable t) {
        ExceptionUtils.handleThrowable(t);
        log.error(sm.getString(
            "contextConfig.authenticatorInstantiate",
            authenticatorName),
            t);
        ok = false;
      }
    }

    if (authenticator != null) {
      Pipeline pipeline = context.getPipeline();
      if (pipeline != null) {
        pipeline.addValve(authenticator);
        if (log.isDebugEnabled()) {
          log.debug(sm.getString(
              "contextConfig.authenticatorConfigured",
              loginConfig.getAuthMethod()));
        }
      }
    }
  }


  /**
   * Create (if necessary) and return a Digester configured to process the context configuration
   * descriptor for an application.
   *
   * @return the digester for context.xml files
   */
  protected Digester createContextDigester() {
    Digester digester = new Digester();
    digester.setValidating(false);
    digester.setRulesValidation(true);
    Map<Class<?>, List<String>> fakeAttributes = new HashMap<>();
    List<String> objectAttrs = new ArrayList<>();
    objectAttrs.add("className");
    fakeAttributes.put(Object.class, objectAttrs);
    // Ignore attribute added by Eclipse for its internal tracking
    List<String> contextAttrs = new ArrayList<>();
    contextAttrs.add("source");
    fakeAttributes.put(StandardContext.class, contextAttrs);
    digester.setFakeAttributes(fakeAttributes);
    RuleSet contextRuleSet = new ContextRuleSet("", false);
    digester.addRuleSet(contextRuleSet);
    RuleSet namingRuleSet = new NamingRuleSet("Context/");
    digester.addRuleSet(namingRuleSet);
    return digester;
  }


  /**
   * Process the default configuration file, if it exists.
   *
   * @param digester The digester that will be used for XML parsing
   */
  protected void contextConfig(Digester digester) {

    String defaultContextXml = null;

    // Open the default context.xml file, if it exists
    if (context instanceof StandardContext) {
      defaultContextXml = ((StandardContext) context).getDefaultContextXml();
    }
    // set the default if we don't have any overrides
    if (defaultContextXml == null) {
      defaultContextXml = Constants.DefaultContextXml;
    }

    if (!context.getOverride()) {
      File defaultContextFile = new File(defaultContextXml);
      if (!defaultContextFile.isAbsolute()) {
        defaultContextFile =
            new File(context.getCatalinaBase(), defaultContextXml);
      }
      if (defaultContextFile.exists()) {
        try {
          URL defaultContextUrl = defaultContextFile.toURI().toURL();
          processContextConfig(digester, defaultContextUrl);
        } catch (MalformedURLException e) {
          log.error(sm.getString(
              "contextConfig.badUrl", defaultContextFile), e);
        }
      }

      File hostContextFile = new File(getHostConfigBase(), Constants.HostContextXml);
      if (hostContextFile.exists()) {
        try {
          URL hostContextUrl = hostContextFile.toURI().toURL();
          processContextConfig(digester, hostContextUrl);
        } catch (MalformedURLException e) {
          log.error(sm.getString(
              "contextConfig.badUrl", hostContextFile), e);
        }
      }
    }
    if (context.getConfigFile() != null) {
      processContextConfig(digester, context.getConfigFile());
    }

  }


  /**
   * Process a context.xml.
   *
   * @param digester   The digester that will be used for XML parsing
   * @param contextXml The URL to the context.xml configuration
   */
  protected void processContextConfig(Digester digester, URL contextXml) {

    if (log.isDebugEnabled()) {
      log.debug("Processing context [" + context.getName()
          + "] configuration file [" + contextXml + "]");
    }

    InputSource source = null;
    InputStream stream = null;

    try {
      source = new InputSource(contextXml.toString());
      URLConnection xmlConn = contextXml.openConnection();
      xmlConn.setUseCaches(false);
      stream = xmlConn.getInputStream();
    } catch (Exception e) {
      log.error(sm.getString("contextConfig.contextMissing",
          contextXml), e);
    }

    if (source == null) {
      return;
    }

    try {
      source.setByteStream(stream);
      digester.setClassLoader(this.getClass().getClassLoader());
      digester.setUseContextClassLoader(false);
      digester.push(context.getParent());
      digester.push(context);
      XmlErrorHandler errorHandler = new XmlErrorHandler();
      digester.setErrorHandler(errorHandler);
      digester.parse(source);
      if (errorHandler.getWarnings().size() > 0 ||
          errorHandler.getErrors().size() > 0) {
        errorHandler.logFindings(log, contextXml.toString());
        ok = false;
      }
      if (log.isDebugEnabled()) {
        log.debug("Successfully processed context [" + context.getName()
            + "] configuration file [" + contextXml + "]");
      }
    } catch (SAXParseException e) {
      log.error(sm.getString("contextConfig.contextParse",
          context.getName()), e);
      log.error(sm.getString("contextConfig.defaultPosition",
          "" + e.getLineNumber(),
          "" + e.getColumnNumber()));
      ok = false;
    } catch (Exception e) {
      log.error(sm.getString("contextConfig.contextParse",
          context.getName()), e);
      ok = false;
    } finally {
      try {
        if (stream != null) {
          stream.close();
        }
      } catch (IOException e) {
        log.error(sm.getString("contextConfig.contextClose"), e);
      }
    }
  }


  /**
   * Adjust docBase.
   *
   * @throws IOException cannot access the context base path
   */
  protected void fixDocBase() throws IOException {

    Host host = (Host) context.getParent();
    File appBase = host.getAppBaseFile();

    // This could be blank, relative, absolute or canonical
    String docBaseConfigured = context.getDocBase();
    // If there is no explicit docBase, derive it from the path and version
    if (docBaseConfigured == null) {
      // Trying to guess the docBase according to the path
      String path = context.getPath();
      if (path == null) {
        return;
      }
      ContextName cn = new ContextName(path, context.getWebappVersion());
      docBaseConfigured = cn.getBaseName();
    }

    // Obtain the absolute docBase in String and File form
    String docBaseAbsolute;
    File docBaseConfiguredFile = new File(docBaseConfigured);
    if (!docBaseConfiguredFile.isAbsolute()) {
      docBaseAbsolute = (new File(appBase, docBaseConfigured)).getAbsolutePath();
    } else {
      docBaseAbsolute = docBaseConfiguredFile.getAbsolutePath();
    }
    File docBaseAbsoluteFile = new File(docBaseAbsolute);
    String originalDocBase = docBaseAbsolute;

    ContextName cn = new ContextName(context.getPath(), context.getWebappVersion());
    String pathName = cn.getBaseName();

    boolean unpackWARs = true;
    if (host instanceof StandardHost) {
      unpackWARs = ((StandardHost) host).isUnpackWARs();
      if (unpackWARs && context instanceof StandardContext) {
        unpackWARs = ((StandardContext) context).getUnpackWAR();
      }
    }

    // At this point we need to determine if we have a WAR file in the
    // appBase that needs to be expanded. Therefore we consider the absolute
    // docBase NOT the canonical docBase. This is because some users symlink
    // WAR files into the appBase and we want this to work correctly.
    boolean docBaseAbsoluteInAppBase = docBaseAbsolute
        .startsWith(appBase.getPath() + File.separatorChar);
    if (docBaseAbsolute.toLowerCase(Locale.ENGLISH).endsWith(".war") && !docBaseAbsoluteFile
        .isDirectory()) {
      URL war = UriUtil.buildJarUrl(docBaseAbsoluteFile);
      if (unpackWARs) {
        docBaseAbsolute = ExpandWar.expand(host, war, pathName);
        docBaseAbsoluteFile = new File(docBaseAbsolute);
        if (context instanceof StandardContext) {
          ((StandardContext) context).setOriginalDocBase(originalDocBase);
        }
      } else {
        ExpandWar.validate(host, war, pathName);
      }
    } else {
      File docBaseAbsoluteFileWar = new File(docBaseAbsolute + ".war");
      URL war = null;
      if (docBaseAbsoluteFileWar.exists() && docBaseAbsoluteInAppBase) {
        war = UriUtil.buildJarUrl(docBaseAbsoluteFileWar);
      }
      if (docBaseAbsoluteFile.exists()) {
        if (war != null && unpackWARs) {
          // Check if WAR needs to be re-expanded (e.g. if it has
          // changed). Note: HostConfig.deployWar() takes care of
          // ensuring that the correct XML file is used.
          // This will be a NO-OP if the WAR is unchanged.
          ExpandWar.expand(host, war, pathName);
        }
      } else {
        if (war != null) {
          if (unpackWARs) {
            docBaseAbsolute = ExpandWar.expand(host, war, pathName);
            docBaseAbsoluteFile = new File(docBaseAbsolute);
          } else {
            docBaseAbsolute = docBaseAbsoluteFileWar.getAbsolutePath();
            docBaseAbsoluteFile = docBaseAbsoluteFileWar;
            ExpandWar.validate(host, war, pathName);
          }
        }
        if (context instanceof StandardContext) {
          ((StandardContext) context).setOriginalDocBase(originalDocBase);
        }
      }
    }

    String docBaseCanonical = docBaseAbsoluteFile.getCanonicalPath();

    // Re-calculate now docBase is a canonical path
    boolean docBaseCanonicalInAppBase = docBaseCanonical
        .startsWith(appBase.getPath() + File.separatorChar);
    String docBase;
    if (docBaseCanonicalInAppBase) {
      docBase = docBaseCanonical.substring(appBase.getPath().length());
      docBase = docBase.replace(File.separatorChar, '/');
      if (docBase.startsWith("/")) {
        docBase = docBase.substring(1);
      }
    } else {
      docBase = docBaseCanonical.replace(File.separatorChar, '/');
    }

    context.setDocBase(docBase);
  }


  protected void antiLocking() {

    if ((context instanceof StandardContext)
        && ((StandardContext) context).getAntiResourceLocking()) {

      Host host = (Host) context.getParent();
      String docBase = context.getDocBase();
      if (docBase == null) {
        return;
      }
      originalDocBase = docBase;

      File docBaseFile = new File(docBase);
      if (!docBaseFile.isAbsolute()) {
        docBaseFile = new File(host.getAppBaseFile(), docBase);
      }

      String path = context.getPath();
      if (path == null) {
        return;
      }
      ContextName cn = new ContextName(path, context.getWebappVersion());
      docBase = cn.getBaseName();

      if (originalDocBase.toLowerCase(Locale.ENGLISH).endsWith(".war")) {
        antiLockingDocBase = new File(
            System.getProperty("java.io.tmpdir"),
            deploymentCount++ + "-" + docBase + ".war");
      } else {
        antiLockingDocBase = new File(
            System.getProperty("java.io.tmpdir"),
            deploymentCount++ + "-" + docBase);
      }
      antiLockingDocBase = antiLockingDocBase.getAbsoluteFile();

      if (log.isDebugEnabled()) {
        log.debug("Anti locking context[" + context.getName()
            + "] setting docBase to " +
            antiLockingDocBase.getPath());
      }

      // Cleanup just in case an old deployment is lying around
      ExpandWar.delete(antiLockingDocBase);
      if (ExpandWar.copy(docBaseFile, antiLockingDocBase)) {
        context.setDocBase(antiLockingDocBase.getPath());
      }
    }
  }


  /**
   * Process a "init" event for this Context.
   */
  protected synchronized void init() {
    // Called from StandardContext.init()

    Digester contextDigester = createContextDigester();
    contextDigester.getParser();

    if (log.isDebugEnabled()) {
      log.debug(sm.getString("contextConfig.init"));
    }
    context.setConfigured(false);
    ok = true;

    contextConfig(contextDigester);
  }


  /**
   * Process a "before start" event for this Context.
   */
  protected synchronized void beforeStart() {

    try {
      fixDocBase();
    } catch (IOException e) {
      log.error(sm.getString(
          "contextConfig.fixDocBase", context.getName()), e);
    }

    antiLocking();
  }


  /**
   * Process a "contextConfig" event for this Context.
   */
  protected synchronized void configureStart() {
    // Called from StandardContext.start()

    if (log.isDebugEnabled()) {
      log.debug(sm.getString("contextConfig.start"));
    }

    if (log.isDebugEnabled()) {
      log.debug(sm.getString("contextConfig.xmlSettings",
          context.getName(),
          Boolean.valueOf(context.getXmlValidation()),
          Boolean.valueOf(context.getXmlNamespaceAware())));
    }

    webConfig();
    context.addServletContainerInitializer(new JasperInitializer(), null);

    if (!context.getIgnoreAnnotations()) {
      applicationAnnotationsConfig();
    }
    if (ok) {
      validateSecurityRoles();
    }

    // Configure an authenticator if we need one
    if (ok) {
      authenticatorConfig();
    }

    // Dump the contents of this pipeline if requested
    if (log.isDebugEnabled()) {
      log.debug("Pipeline Configuration:");
      Pipeline pipeline = context.getPipeline();
      Valve valves[] = null;
      if (pipeline != null) {
        valves = pipeline.getValves();
      }
      if (valves != null) {
        for (int i = 0; i < valves.length; i++) {
          log.debug("  " + valves[i].getClass().getName());
        }
      }
      log.debug("======================");
    }

    // Make our application available if no problems were encountered
    if (ok) {
      context.setConfigured(true);
    } else {
      log.error(sm.getString("contextConfig.unavailable"));
      context.setConfigured(false);
    }

  }


  /**
   * Process a "stop" event for this Context.
   */
  protected synchronized void configureStop() {

    if (log.isDebugEnabled()) {
      log.debug(sm.getString("contextConfig.stop"));
    }

    int i;

    // Removing children
    Container[] children = context.findChildren();
    for (i = 0; i < children.length; i++) {
      context.removeChild(children[i]);
    }

    // Removing application parameters
        /*
        ApplicationParameter[] applicationParameters =
            context.findApplicationParameters();
        for (i = 0; i < applicationParameters.length; i++) {
            context.removeApplicationParameter
                (applicationParameters[i].getName());
        }
        */

    // Removing security constraints
    SecurityConstraint[] securityConstraints = context.findConstraints();
    for (i = 0; i < securityConstraints.length; i++) {
      context.removeConstraint(securityConstraints[i]);
    }

    // Removing Ejbs
        /*
        ContextEjb[] contextEjbs = context.findEjbs();
        for (i = 0; i < contextEjbs.length; i++) {
            context.removeEjb(contextEjbs[i].getName());
        }
        */

    // Removing environments
        /*
        ContextEnvironment[] contextEnvironments = context.findEnvironments();
        for (i = 0; i < contextEnvironments.length; i++) {
            context.removeEnvironment(contextEnvironments[i].getName());
        }
        */

    // Removing errors pages
    ErrorPage[] errorPages = context.findErrorPages();
    for (i = 0; i < errorPages.length; i++) {
      context.removeErrorPage(errorPages[i]);
    }

    // Removing filter defs
    FilterDef[] filterDefs = context.findFilterDefs();
    for (i = 0; i < filterDefs.length; i++) {
      context.removeFilterDef(filterDefs[i]);
    }

    // Removing filter maps
    FilterMap[] filterMaps = context.findFilterMaps();
    for (i = 0; i < filterMaps.length; i++) {
      context.removeFilterMap(filterMaps[i]);
    }

    // Removing local ejbs
        /*
        ContextLocalEjb[] contextLocalEjbs = context.findLocalEjbs();
        for (i = 0; i < contextLocalEjbs.length; i++) {
            context.removeLocalEjb(contextLocalEjbs[i].getName());
        }
        */

    // Removing Mime mappings
    String[] mimeMappings = context.findMimeMappings();
    for (i = 0; i < mimeMappings.length; i++) {
      context.removeMimeMapping(mimeMappings[i]);
    }

    // Removing parameters
    String[] parameters = context.findParameters();
    for (i = 0; i < parameters.length; i++) {
      context.removeParameter(parameters[i]);
    }

    // Removing resource env refs
        /*
        String[] resourceEnvRefs = context.findResourceEnvRefs();
        for (i = 0; i < resourceEnvRefs.length; i++) {
            context.removeResourceEnvRef(resourceEnvRefs[i]);
        }
        */

    // Removing resource links
        /*
        ContextResourceLink[] contextResourceLinks =
            context.findResourceLinks();
        for (i = 0; i < contextResourceLinks.length; i++) {
            context.removeResourceLink(contextResourceLinks[i].getName());
        }
        */

    // Removing resources
        /*
        ContextResource[] contextResources = context.findResources();
        for (i = 0; i < contextResources.length; i++) {
            context.removeResource(contextResources[i].getName());
        }
        */

    // Removing security role
    String[] securityRoles = context.findSecurityRoles();
    for (i = 0; i < securityRoles.length; i++) {
      context.removeSecurityRole(securityRoles[i]);
    }

    // Removing servlet mappings
    String[] servletMappings = context.findServletMappings();
    for (i = 0; i < servletMappings.length; i++) {
      context.removeServletMapping(servletMappings[i]);
    }

    // FIXME : Removing status pages

    // Removing welcome files
    String[] welcomeFiles = context.findWelcomeFiles();
    for (i = 0; i < welcomeFiles.length; i++) {
      context.removeWelcomeFile(welcomeFiles[i]);
    }

    // Removing wrapper lifecycles
    String[] wrapperLifecycles = context.findWrapperLifecycles();
    for (i = 0; i < wrapperLifecycles.length; i++) {
      context.removeWrapperLifecycle(wrapperLifecycles[i]);
    }

    // Removing wrapper listeners
    String[] wrapperListeners = context.findWrapperListeners();
    for (i = 0; i < wrapperListeners.length; i++) {
      context.removeWrapperListener(wrapperListeners[i]);
    }

    // Remove (partially) folders and files created by antiLocking
    if (antiLockingDocBase != null) {
      // No need to log failure - it is expected in this case
      ExpandWar.delete(antiLockingDocBase, false);
    }

    // Reset ServletContextInitializer scanning
    initializerClassMap.clear();
    typeInitializerMap.clear();

    ok = true;

  }


  /**
   * Process a "destroy" event for this Context.
   */
  protected synchronized void destroy() {
    // Called from StandardContext.destroy()
    if (log.isDebugEnabled()) {
      log.debug(sm.getString("contextConfig.destroy"));
    }

    // Skip clearing the work directory if Tomcat is being shutdown
    Server s = getServer();
    if (s != null && !s.getState().isAvailable()) {
      return;
    }

    // Changed to getWorkPath per Bugzilla 35819.
    if (context instanceof StandardContext) {
      String workDir = ((StandardContext) context).getWorkPath();
      if (workDir != null) {
        ExpandWar.delete(new File(workDir));
      }
    }
  }


  private Server getServer() {
    Container c = context;
    while (c != null && !(c instanceof Engine)) {
      c = c.getParent();
    }

    if (c == null) {
      return null;
    }

    Service s = ((Engine) c).getService();

    if (s == null) {
      return null;
    }

    return s.getServer();
  }

  /**
   * Validate the usage of security role names in the web application deployment descriptor.  If any
   * problems are found, issue warning messages (for backwards compatibility) and add the missing
   * roles. (To make these problems fatal instead, simply set the <code>ok</code> instance variable
   * to <code>false</code> as well).
   */
  protected void validateSecurityRoles() {

    // Check role names used in <security-constraint> elements
    SecurityConstraint constraints[] = context.findConstraints();
    for (int i = 0; i < constraints.length; i++) {
      String roles[] = constraints[i].findAuthRoles();
      for (int j = 0; j < roles.length; j++) {
        if (!"*".equals(roles[j]) &&
            !context.findSecurityRole(roles[j])) {
          log.warn(sm.getString("contextConfig.role.auth", roles[j]));
          context.addSecurityRole(roles[j]);
        }
      }
    }

    // Check role names used in <servlet> elements
    Container wrappers[] = context.findChildren();
    for (int i = 0; i < wrappers.length; i++) {
      Wrapper wrapper = (Wrapper) wrappers[i];
      String runAs = wrapper.getRunAs();
      if ((runAs != null) && !context.findSecurityRole(runAs)) {
        log.warn(sm.getString("contextConfig.role.runas", runAs));
        context.addSecurityRole(runAs);
      }
      String names[] = wrapper.findSecurityReferences();
      for (int j = 0; j < names.length; j++) {
        String link = wrapper.findSecurityReference(names[j]);
        if ((link != null) && !context.findSecurityRole(link)) {
          log.warn(sm.getString("contextConfig.role.link", link));
          context.addSecurityRole(link);
        }
      }
    }

  }


  protected File getHostConfigBase() {
    File file = null;
    if (context.getParent() instanceof Host) {
      file = ((Host) context.getParent()).getConfigBaseFile();
    }
    return file;
  }

  /**
   * Scan the web.xml files that apply to the web application and merge them using the rules defined
   * in the spec. For the global web.xml files, where there is duplicate configuration, the most
   * specific level wins. ie an application's web.xml takes precedence over the host level or global
   * web.xml file.
   */
  protected void webConfig() {
    /*
     * Anything and everything can override the global and host defaults.
     * This is implemented in two parts
     * - Handle as a web fragment that gets added after everything else so
     *   everything else takes priority
     * - Mark Servlets as overridable so SCI configuration can replace
     *   configuration from the defaults
     */

    /*
     * The rules for annotation scanning are not as clear-cut as one might
     * think. Tomcat implements the following process:
     * - As per SRV.1.6.2, Tomcat will scan for annotations regardless of
     *   which Servlet spec version is declared in web.xml. The EG has
     *   confirmed this is the expected behaviour.
     * - As per http://java.net/jira/browse/SERVLET_SPEC-36, if the main
     *   web.xml is marked as metadata-complete, JARs are still processed
     *   for SCIs.
     * - If metadata-complete=true and an absolute ordering is specified,
     *   JARs excluded from the ordering are also excluded from the SCI
     *   processing.
     * - If an SCI has a @HandlesType annotation then all classes (except
     *   those in JARs excluded from an absolute ordering) need to be
     *   scanned to check if they match.
     */
    WebXmlParser webXmlParser = new WebXmlParser(context.getXmlNamespaceAware(),
        context.getXmlValidation(), context.getXmlBlockExternal());

    Set<WebXml> defaults = new HashSet<>();
    defaults.add(getDefaultWebXmlFragment(webXmlParser));

    WebXml webXml = createWebXml();

    // Parse context level web.xml
    InputSource contextWebXml = getContextWebXmlSource();
    if (!webXmlParser.parseWebXml(contextWebXml, webXml, false)) {
      ok = false;
    }

    ServletContext sContext = context.getServletContext();

    // Ordering is important here

    // Step 1. Identify all the JARs packaged with the application and those
    // provided by the container. If any of the application JARs have a
    // web-fragment.xml it will be parsed at this point. web-fragment.xml
    // files are ignored for container provided JARs.
    Map<String, WebXml> fragments = processJarsForWebFragments(webXml, webXmlParser);

    // Step 2. Order the fragments.
    Set<WebXml> orderedFragments = null;
    orderedFragments =
        WebXml.orderWebFragments(webXml, fragments, sContext);

    // Step 3. Look for ServletContainerInitializer implementations
    if (ok) {
      processServletContainerInitializers();
    }

    if (!webXml.isMetadataComplete() || typeInitializerMap.size() > 0) {
      // Steps 4 & 5.
      processClasses(webXml, orderedFragments);
    }

    if (!webXml.isMetadataComplete()) {
      // Step 6. Merge web-fragment.xml files into the main web.xml
      // file.
      if (ok) {
        ok = webXml.merge(orderedFragments);
      }

      // Step 7. Apply global defaults
      // Have to merge defaults before JSP conversion since defaults
      // provide JSP servlet definition.
      webXml.merge(defaults);

      // Step 8. Convert explicitly mentioned jsps to servlets
      if (ok) {
        convertJsps(webXml);
      }

      // Step 9. Apply merged web.xml to Context
      if (ok) {
        configureContext(webXml);
      }
    } else {
      webXml.merge(defaults);
      convertJsps(webXml);
      configureContext(webXml);
    }

    if (context.getLogEffectiveWebXml()) {
      log.info("web.xml:\n" + webXml.toXml());
    }

    // Always need to look for static resources
    // Step 10. Look for static resources packaged in JARs
    if (ok) {
      // Spec does not define an order.
      // Use ordered JARs followed by remaining JARs
      Set<WebXml> resourceJars = new LinkedHashSet<>();
      for (WebXml fragment : orderedFragments) {
        resourceJars.add(fragment);
      }
      for (WebXml fragment : fragments.values()) {
        if (!resourceJars.contains(fragment)) {
          resourceJars.add(fragment);
        }
      }
      processResourceJARs(resourceJars);
      // See also StandardContext.resourcesStart() for
      // WEB-INF/classes/META-INF/resources configuration
    }

    // Step 11. Apply the ServletContainerInitializer config to the
    // context
    if (ok) {
      for (Map.Entry<ServletContainerInitializer,
          Set<Class<?>>> entry :
          initializerClassMap.entrySet()) {
        if (entry.getValue().isEmpty()) {
          context.addServletContainerInitializer(
              entry.getKey(), null);
        } else {
          context.addServletContainerInitializer(
              entry.getKey(), entry.getValue());
        }
      }
    }
  }


  protected void processClasses(WebXml webXml, Set<WebXml> orderedFragments) {
    // Step 4. Process /WEB-INF/classes for annotations and
    // @HandlesTypes matches
    Map<String, JavaClassCacheEntry> javaClassCache = new HashMap<>();

    if (ok) {
      WebResource[] webResources =
          context.getResources().listResources("/WEB-INF/classes");

      for (WebResource webResource : webResources) {
        // Skip the META-INF directory from any JARs that have been
        // expanded in to WEB-INF/classes (sometimes IDEs do this).
        if ("META-INF".equals(webResource.getName())) {
          continue;
        }
        processAnnotationsWebResource(webResource, webXml,
            webXml.isMetadataComplete(), javaClassCache);
      }
    }

    // Step 5. Process JARs for annotations and
    // @HandlesTypes matches - only need to process those fragments we
    // are going to use (remember orderedFragments includes any
    // container fragments)
    if (ok) {
      processAnnotations(
          orderedFragments, webXml.isMetadataComplete(), javaClassCache);
    }

    // Cache, if used, is no longer required so clear it
    javaClassCache.clear();
  }


  private void configureContext(WebXml webxml) {
    // As far as possible, process in alphabetical order so it is easy to
    // check everything is present
    // Some validation depends on correct public ID
    context.setPublicId(webxml.getPublicId());

    // Everything else in order
    context.setEffectiveMajorVersion(webxml.getMajorVersion());
    context.setEffectiveMinorVersion(webxml.getMinorVersion());

    for (Entry<String, String> entry : webxml.getContextParams().entrySet()) {
      context.addParameter(entry.getKey(), entry.getValue());
    }
    context.setDenyUncoveredHttpMethods(
        webxml.getDenyUncoveredHttpMethods());
    context.setDisplayName(webxml.getDisplayName());
    context.setDistributable(webxml.isDistributable());
    for (ContextLocalEjb ejbLocalRef : webxml.getEjbLocalRefs().values()) {
      context.getNamingResources().addLocalEjb(ejbLocalRef);
    }
    for (ContextEjb ejbRef : webxml.getEjbRefs().values()) {
      context.getNamingResources().addEjb(ejbRef);
    }
    for (ContextEnvironment environment : webxml.getEnvEntries().values()) {
      context.getNamingResources().addEnvironment(environment);
    }
    for (ErrorPage errorPage : webxml.getErrorPages().values()) {
      context.addErrorPage(errorPage);
    }
    for (FilterDef filter : webxml.getFilters().values()) {
      if (filter.getAsyncSupported() == null) {
        filter.setAsyncSupported("false");
      }
      context.addFilterDef(filter);
    }
    for (FilterMap filterMap : webxml.getFilterMappings()) {
      context.addFilterMap(filterMap);
    }
    context.setJspConfigDescriptor(webxml.getJspConfigDescriptor());
    for (String listener : webxml.getListeners()) {
      context.addApplicationListener(listener);
    }
    for (Entry<String, String> entry :
        webxml.getLocaleEncodingMappings().entrySet()) {
      context.addLocaleEncodingMappingParameter(entry.getKey(),
          entry.getValue());
    }
    // Prevents IAE
    if (webxml.getLoginConfig() != null) {
      context.setLoginConfig(webxml.getLoginConfig());
    }
    for (MessageDestinationRef mdr :
        webxml.getMessageDestinationRefs().values()) {
      context.getNamingResources().addMessageDestinationRef(mdr);
    }

    // messageDestinations were ignored in Tomcat 6, so ignore here

    context.setIgnoreAnnotations(webxml.isMetadataComplete());
    for (Entry<String, String> entry :
        webxml.getMimeMappings().entrySet()) {
      context.addMimeMapping(entry.getKey(), entry.getValue());
    }
    // Name is just used for ordering
    for (ContextResourceEnvRef resource :
        webxml.getResourceEnvRefs().values()) {
      context.getNamingResources().addResourceEnvRef(resource);
    }
    for (ContextResource resource : webxml.getResourceRefs().values()) {
      context.getNamingResources().addResource(resource);
    }
    boolean allAuthenticatedUsersIsAppRole =
        webxml.getSecurityRoles().contains(
            SecurityConstraint.ROLE_ALL_AUTHENTICATED_USERS);
    for (SecurityConstraint constraint : webxml.getSecurityConstraints()) {
      if (allAuthenticatedUsersIsAppRole) {
        constraint.treatAllAuthenticatedUsersAsApplicationRole();
      }
      context.addConstraint(constraint);
    }
    for (String role : webxml.getSecurityRoles()) {
      context.addSecurityRole(role);
    }
    for (ContextService service : webxml.getServiceRefs().values()) {
      context.getNamingResources().addService(service);
    }
    for (ServletDef servlet : webxml.getServlets().values()) {
      Wrapper wrapper = context.createWrapper();
      // Description is ignored
      // Display name is ignored
      // Icons are ignored

      // jsp-file gets passed to the JSP Servlet as an init-param

      if (servlet.getLoadOnStartup() != null) {
        wrapper.setLoadOnStartup(servlet.getLoadOnStartup().intValue());
      }
      if (servlet.getEnabled() != null) {
        wrapper.setEnabled(servlet.getEnabled().booleanValue());
      }
      wrapper.setName(servlet.getServletName());
      Map<String, String> params = servlet.getParameterMap();
      for (Entry<String, String> entry : params.entrySet()) {
        wrapper.addInitParameter(entry.getKey(), entry.getValue());
      }
      wrapper.setRunAs(servlet.getRunAs());
      Set<SecurityRoleRef> roleRefs = servlet.getSecurityRoleRefs();
      for (SecurityRoleRef roleRef : roleRefs) {
        wrapper.addSecurityReference(
            roleRef.getName(), roleRef.getLink());
      }
      wrapper.setServletClass(servlet.getServletClass());
      MultipartDef multipartdef = servlet.getMultipartDef();
      if (multipartdef != null) {
        if (multipartdef.getMaxFileSize() != null &&
            multipartdef.getMaxRequestSize() != null &&
            multipartdef.getFileSizeThreshold() != null) {
          wrapper.setMultipartConfigElement(new MultipartConfigElement(
              multipartdef.getLocation(),
              Long.parseLong(multipartdef.getMaxFileSize()),
              Long.parseLong(multipartdef.getMaxRequestSize()),
              Integer.parseInt(
                  multipartdef.getFileSizeThreshold())));
        } else {
          wrapper.setMultipartConfigElement(new MultipartConfigElement(
              multipartdef.getLocation()));
        }
      }
      if (servlet.getAsyncSupported() != null) {
        wrapper.setAsyncSupported(
            servlet.getAsyncSupported().booleanValue());
      }
      wrapper.setOverridable(servlet.isOverridable());
      context.addChild(wrapper);
    }
    for (Entry<String, String> entry :
        webxml.getServletMappings().entrySet()) {
      context.addServletMappingDecoded(entry.getKey(), entry.getValue());
    }
    SessionConfig sessionConfig = webxml.getSessionConfig();
    if (sessionConfig != null) {
      if (sessionConfig.getSessionTimeout() != null) {
        context.setSessionTimeout(
            sessionConfig.getSessionTimeout().intValue());
      }
      SessionCookieConfig scc =
          context.getServletContext().getSessionCookieConfig();
      scc.setName(sessionConfig.getCookieName());
      scc.setDomain(sessionConfig.getCookieDomain());
      scc.setPath(sessionConfig.getCookiePath());
      scc.setComment(sessionConfig.getCookieComment());
      if (sessionConfig.getCookieHttpOnly() != null) {
        scc.setHttpOnly(sessionConfig.getCookieHttpOnly().booleanValue());
      }
      if (sessionConfig.getCookieSecure() != null) {
        scc.setSecure(sessionConfig.getCookieSecure().booleanValue());
      }
      if (sessionConfig.getCookieMaxAge() != null) {
        scc.setMaxAge(sessionConfig.getCookieMaxAge().intValue());
      }
      if (sessionConfig.getSessionTrackingModes().size() > 0) {
        context.getServletContext().setSessionTrackingModes(
            sessionConfig.getSessionTrackingModes());
      }
    }

    // Context doesn't use version directly

    for (String welcomeFile : webxml.getWelcomeFiles()) {
      /*
       * The following will result in a welcome file of "" so don't add
       * that to the context
       * <welcome-file-list>
       *   <welcome-file/>
       * </welcome-file-list>
       */
      if (welcomeFile != null && welcomeFile.length() > 0) {
        context.addWelcomeFile(welcomeFile);
      }
    }

    // Do this last as it depends on servlets
    for (JspPropertyGroup jspPropertyGroup :
        webxml.getJspPropertyGroups()) {
      String jspServletName = context.findServletMapping("*.jsp");
      if (jspServletName == null) {
        jspServletName = "jsp";
      }
      if (context.findChild(jspServletName) != null) {
        for (String urlPattern : jspPropertyGroup.getUrlPatterns()) {
          context.addServletMappingDecoded(urlPattern, jspServletName, true);
        }
      } else {
        if (log.isDebugEnabled()) {
          for (String urlPattern : jspPropertyGroup.getUrlPatterns()) {
            log.debug("Skipping " + urlPattern + " , no servlet " +
                jspServletName);
          }
        }
      }
    }

    for (Entry<String, String> entry :
        webxml.getPostConstructMethods().entrySet()) {
      context.addPostConstructMethod(entry.getKey(), entry.getValue());
    }

    for (Entry<String, String> entry :
        webxml.getPreDestroyMethods().entrySet()) {
      context.addPreDestroyMethod(entry.getKey(), entry.getValue());
    }
  }


  private WebXml getDefaultWebXmlFragment(WebXmlParser webXmlParser) {

    // Host should never be null
    Host host = (Host) context.getParent();

    DefaultWebXmlCacheEntry entry = hostWebXmlCache.get(host);

    InputSource globalWebXml = getGlobalWebXmlSource();
    InputSource hostWebXml = getHostWebXmlSource();

    long globalTimeStamp = 0;
    long hostTimeStamp = 0;

    if (globalWebXml != null) {
      URLConnection uc = null;
      try {
        URL url = new URL(globalWebXml.getSystemId());
        uc = url.openConnection();
        globalTimeStamp = uc.getLastModified();
      } catch (IOException e) {
        globalTimeStamp = -1;
      } finally {
        if (uc != null) {
          try {
            uc.getInputStream().close();
          } catch (IOException e) {
            ExceptionUtils.handleThrowable(e);
            globalTimeStamp = -1;
          }
        }
      }
    }

    if (hostWebXml != null) {
      URLConnection uc = null;
      try {
        URL url = new URL(hostWebXml.getSystemId());
        uc = url.openConnection();
        hostTimeStamp = uc.getLastModified();
      } catch (IOException e) {
        hostTimeStamp = -1;
      } finally {
        if (uc != null) {
          try {
            uc.getInputStream().close();
          } catch (IOException e) {
            ExceptionUtils.handleThrowable(e);
            hostTimeStamp = -1;
          }
        }
      }
    }

    if (entry != null && entry.getGlobalTimeStamp() == globalTimeStamp &&
        entry.getHostTimeStamp() == hostTimeStamp) {
      InputSourceUtil.close(globalWebXml);
      InputSourceUtil.close(hostWebXml);
      return entry.getWebXml();
    }

    // Parsing global web.xml is relatively expensive. Use a sync block to
    // make sure it only happens once. Use the pipeline since a lock will
    // already be held on the host by another thread
    synchronized (host.getPipeline()) {
      entry = hostWebXmlCache.get(host);
      if (entry != null && entry.getGlobalTimeStamp() == globalTimeStamp &&
          entry.getHostTimeStamp() == hostTimeStamp) {
        return entry.getWebXml();
      }

      WebXml webXmlDefaultFragment = createWebXml();
      webXmlDefaultFragment.setOverridable(true);
      // Set to distributable else every app will be prevented from being
      // distributable when the default fragment is merged with the main
      // web.xml
      webXmlDefaultFragment.setDistributable(true);
      // When merging, the default welcome files are only used if the app has
      // not defined any welcomes files.
      webXmlDefaultFragment.setAlwaysAddWelcomeFiles(false);

      // Parse global web.xml if present
      if (globalWebXml == null) {
        // This is unusual enough to log
        log.info(sm.getString("contextConfig.defaultMissing"));
      } else {
        if (!webXmlParser.parseWebXml(
            globalWebXml, webXmlDefaultFragment, false)) {
          ok = false;
        }
      }

      // Parse host level web.xml if present
      // Additive apart from welcome pages
      webXmlDefaultFragment.setReplaceWelcomeFiles(true);

      if (!webXmlParser.parseWebXml(
          hostWebXml, webXmlDefaultFragment, false)) {
        ok = false;
      }

      // Don't update the cache if an error occurs
      if (globalTimeStamp != -1 && hostTimeStamp != -1) {
        entry = new DefaultWebXmlCacheEntry(webXmlDefaultFragment,
            globalTimeStamp, hostTimeStamp);
        hostWebXmlCache.put(host, entry);
        // Add a Lifecycle listener to the Host that will remove it from
        // the hostWebXmlCache once the Host is destroyed
        host.addLifecycleListener(new HostWebXmlCacheCleaner());
      }

      return webXmlDefaultFragment;
    }
  }


  private void convertJsps(WebXml webXml) {
    Map<String, String> jspInitParams;
    ServletDef jspServlet = webXml.getServlets().get("jsp");
    if (jspServlet == null) {
      jspInitParams = new HashMap<>();
      Wrapper w = (Wrapper) context.findChild("jsp");
      if (w != null) {
        String[] params = w.findInitParameters();
        for (String param : params) {
          jspInitParams.put(param, w.findInitParameter(param));
        }
      }
    } else {
      jspInitParams = jspServlet.getParameterMap();
    }
    for (ServletDef servletDef : webXml.getServlets().values()) {
      if (servletDef.getJspFile() != null) {
        convertJsp(servletDef, jspInitParams);
      }
    }
  }

  private void convertJsp(ServletDef servletDef,
      Map<String, String> jspInitParams) {
    servletDef.setServletClass(org.apache.catalina.core.Constants.JSP_SERVLET_CLASS);
    String jspFile = servletDef.getJspFile();
    if ((jspFile != null) && !jspFile.startsWith("/")) {
      if (context.isServlet22()) {
        if (log.isDebugEnabled()) {
          log.debug(sm.getString("contextConfig.jspFile.warning",
              jspFile));
        }
        jspFile = "/" + jspFile;
      } else {
        throw new IllegalArgumentException
            (sm.getString("contextConfig.jspFile.error", jspFile));
      }
    }
    servletDef.getParameterMap().put("jspFile", jspFile);
    servletDef.setJspFile(null);
    for (Map.Entry<String, String> initParam : jspInitParams.entrySet()) {
      servletDef.addInitParameter(initParam.getKey(), initParam.getValue());
    }
  }

  protected WebXml createWebXml() {
    return new WebXml();
  }

  /**
   * Scan JARs for ServletContainerInitializer implementations.
   */
  protected void processServletContainerInitializers() {

    List<ServletContainerInitializer> detectedScis;
    try {
      WebappServiceLoader<ServletContainerInitializer> loader = new WebappServiceLoader<>(context);
      detectedScis = loader.load(ServletContainerInitializer.class);
    } catch (IOException e) {
      log.error(sm.getString(
          "contextConfig.servletContainerInitializerFail",
          context.getName()),
          e);
      ok = false;
      return;
    }

    for (ServletContainerInitializer sci : detectedScis) {
      initializerClassMap.put(sci, new HashSet<Class<?>>());

      HandlesTypes ht;
      try {
        ht = sci.getClass().getAnnotation(HandlesTypes.class);
      } catch (Exception e) {
        if (log.isDebugEnabled()) {
          log.info(sm.getString("contextConfig.sci.debug",
              sci.getClass().getName()),
              e);
        } else {
          log.info(sm.getString("contextConfig.sci.info",
              sci.getClass().getName()));
        }
        continue;
      }
      if (ht == null) {
        continue;
      }
      Class<?>[] types = ht.value();
      if (types == null) {
        continue;
      }

      for (Class<?> type : types) {
        if (type.isAnnotation()) {
          handlesTypesAnnotations = true;
        } else {
          handlesTypesNonAnnotations = true;
        }
        Set<ServletContainerInitializer> scis =
            typeInitializerMap.get(type);
        if (scis == null) {
          scis = new HashSet<>();
          typeInitializerMap.put(type, scis);
        }
        scis.add(sci);
      }
    }
  }


  /**
   * Scan JARs that contain web-fragment.xml files that will be used to configure this application
   * to see if they also contain static resources. If static resources are found, add them to the
   * context. Resources are added in web-fragment.xml priority order.
   *
   * @param fragments The set of fragments that will be scanned for static resources
   */
  protected void processResourceJARs(Set<WebXml> fragments) {
    for (WebXml fragment : fragments) {
      URL url = fragment.getURL();
      try {
        if ("jar".equals(url.getProtocol()) || url.toString().endsWith(".jar")) {
          try (Jar jar = JarFactory.newInstance(url)) {
            jar.nextEntry();
            String entryName = jar.getEntryName();
            while (entryName != null) {
              if (entryName.startsWith("META-INF/resources/")) {
                context.getResources().createWebResourceSet(
                    WebResourceRoot.ResourceSetType.RESOURCE_JAR,
                    "/", url, "/META-INF/resources");
                break;
              }
              jar.nextEntry();
              entryName = jar.getEntryName();
            }
          }
        } else if ("file".equals(url.getProtocol())) {
          File file = new File(url.toURI());
          File resources = new File(file, "META-INF/resources/");
          if (resources.isDirectory()) {
            context.getResources().createWebResourceSet(
                WebResourceRoot.ResourceSetType.RESOURCE_JAR,
                "/", resources.getAbsolutePath(), null, "/");
          }
        }
      } catch (IOException ioe) {
        log.error(sm.getString("contextConfig.resourceJarFail", url,
            context.getName()));
      } catch (URISyntaxException e) {
        log.error(sm.getString("contextConfig.resourceJarFail", url,
            context.getName()));
      }
    }
  }


  /**
   * Identify the default web.xml to be used and obtain an input source for it.
   *
   * @return an input source to the default web.xml
   */
  protected InputSource getGlobalWebXmlSource() {
    // Is a default web.xml specified for the Context?
    if (defaultWebXml == null && context instanceof StandardContext) {
      defaultWebXml = ((StandardContext) context).getDefaultWebXml();
    }
    // Set the default if we don't have any overrides
    if (defaultWebXml == null) {
      getDefaultWebXml();
    }

    // Is it explicitly suppressed, e.g. in embedded environment?
    if (Constants.NoDefaultWebXml.equals(defaultWebXml)) {
      return null;
    }
    return getWebXmlSource(defaultWebXml,
        context.getCatalinaBase().getPath());
  }


  /**
   * Identify the host web.xml to be used and obtain an input source for it.
   *
   * @return an input source to the default per host web.xml
   */
  protected InputSource getHostWebXmlSource() {
    File hostConfigBase = getHostConfigBase();
    if (hostConfigBase == null) {
      return null;
    }

    return getWebXmlSource(Constants.HostWebXml, hostConfigBase.getPath());
  }

  /**
   * Identify the application web.xml to be used and obtain an input source for it.
   *
   * @return an input source to the context web.xml
   */
  protected InputSource getContextWebXmlSource() {
    InputStream stream = null;
    InputSource source = null;
    URL url = null;

    String altDDName = null;

    // Open the application web.xml file, if it exists
    ServletContext servletContext = context.getServletContext();
    try {
      if (servletContext != null) {
        altDDName = (String) servletContext.getAttribute(Globals.ALT_DD_ATTR);
        if (altDDName != null) {
          try {
            stream = new FileInputStream(altDDName);
            url = new File(altDDName).toURI().toURL();
          } catch (FileNotFoundException e) {
            log.error(sm.getString("contextConfig.altDDNotFound",
                altDDName));
          } catch (MalformedURLException e) {
            log.error(sm.getString("contextConfig.applicationUrl"));
          }
        } else {
          stream = servletContext.getResourceAsStream
              (Constants.ApplicationWebXml);
          try {
            url = servletContext.getResource(
                Constants.ApplicationWebXml);
          } catch (MalformedURLException e) {
            log.error(sm.getString("contextConfig.applicationUrl"));
          }
        }
      }
      if (stream == null || url == null) {
        if (log.isDebugEnabled()) {
          log.debug(sm.getString("contextConfig.applicationMissing") + " " + context);
        }
      } else {
        source = new InputSource(url.toExternalForm());
        source.setByteStream(stream);
      }
    } finally {
      if (source == null && stream != null) {
        try {
          stream.close();
        } catch (IOException e) {
          // Ignore
        }
      }
    }

    return source;
  }

  /**
   * Utility method to create an input source from the specified XML file.
   *
   * @param filename Name of the file (possibly with one or more leading path segments) to read
   * @param path     Location that filename is relative to
   * @return the input source
   */
  protected InputSource getWebXmlSource(String filename, String path) {
    File file = new File(filename);
    if (!file.isAbsolute()) {
      file = new File(path, filename);
    }

    InputStream stream = null;
    InputSource source = null;

    try {
      if (!file.exists()) {
        // Use getResource and getResourceAsStream
        stream =
            getClass().getClassLoader().getResourceAsStream(filename);
        if (stream != null) {
          source =
              new InputSource(getClass().getClassLoader().getResource(
                  filename).toURI().toString());
        }
      } else {
        source = new InputSource(file.getAbsoluteFile().toURI().toString());
        stream = new FileInputStream(file);
      }

      if (stream != null && source != null) {
        source.setByteStream(stream);
      }
    } catch (Exception e) {
      log.error(sm.getString(
          "contextConfig.defaultError", filename, file), e);
    } finally {
      if (source == null && stream != null) {
        try {
          stream.close();
        } catch (IOException e) {
          // Ignore
        }
      }
    }

    return source;
  }


  /**
   * Scan /WEB-INF/lib for JARs and for each one found add it and any /META-INF/web-fragment.xml to
   * the resulting Map. web-fragment.xml files will be parsed before being added to the map. Every
   * JAR will be added and
   * <code>null</code> will be used if no web-fragment.xml was found. Any JARs
   * known not contain fragments will be skipped.
   *
   * @param application  The main web.xml metadata
   * @param webXmlParser The parser to use to process the web.xml file
   * @return A map of JAR name to processed web fragment (if any)
   */
  protected Map<String, WebXml> processJarsForWebFragments(WebXml application,
      WebXmlParser webXmlParser) {

    JarScanner jarScanner = context.getJarScanner();
    boolean delegate = false;
    if (context instanceof StandardContext) {
      delegate = ((StandardContext) context).getDelegate();
    }
    boolean parseRequired = true;
    Set<String> absoluteOrder = application.getAbsoluteOrdering();
    if (absoluteOrder != null && absoluteOrder.isEmpty() &&
        !context.getXmlValidation()) {
      // Skip parsing when there is an empty absolute ordering and
      // validation is not enabled
      parseRequired = false;
    }

    FragmentJarScannerCallback callback =
        new FragmentJarScannerCallback(webXmlParser, delegate, parseRequired);

    jarScanner.scan(JarScanType.PLUGGABILITY,
        context.getServletContext(), callback);

    if (!callback.isOk()) {
      ok = false;
    }
    return callback.getFragments();
  }

  protected void processAnnotations(Set<WebXml> fragments,
      boolean handlesTypesOnly, Map<String, JavaClassCacheEntry> javaClassCache) {
    for (WebXml fragment : fragments) {
      // Only need to scan for @HandlesTypes matches if any of the
      // following are true:
      // - it has already been determined only @HandlesTypes is required
      //   (e.g. main web.xml has metadata-complete="true"
      // - this fragment is for a container JAR (Servlet 3.1 section 8.1)
      // - this fragment has metadata-complete="true"
      boolean htOnly = handlesTypesOnly || !fragment.getWebappJar() ||
          fragment.isMetadataComplete();

      WebXml annotations = new WebXml();
      // no impact on distributable
      annotations.setDistributable(true);
      URL url = fragment.getURL();
      processAnnotationsUrl(url, annotations, htOnly, javaClassCache);
      Set<WebXml> set = new HashSet<>();
      set.add(annotations);
      // Merge annotations into fragment - fragment takes priority
      fragment.merge(set);
    }
  }

  protected void processAnnotationsWebResource(WebResource webResource,
      WebXml fragment, boolean handlesTypesOnly,
      Map<String, JavaClassCacheEntry> javaClassCache) {

    if (webResource.isDirectory()) {
      WebResource[] webResources =
          webResource.getWebResourceRoot().listResources(
              webResource.getWebappPath());
      if (webResources.length > 0) {
        if (log.isDebugEnabled()) {
          log.debug(sm.getString(
              "contextConfig.processAnnotationsWebDir.debug",
              webResource.getURL()));
        }
        for (WebResource r : webResources) {
          processAnnotationsWebResource(r, fragment, handlesTypesOnly, javaClassCache);
        }
      }
    } else if (webResource.isFile() &&
        webResource.getName().endsWith(".class")) {
      try (InputStream is = webResource.getInputStream()) {
        processAnnotationsStream(is, fragment, handlesTypesOnly, javaClassCache);
      } catch (IOException e) {
        log.error(sm.getString("contextConfig.inputStreamWebResource",
            webResource.getWebappPath()), e);
      } catch (ClassFormatException e) {
        log.error(sm.getString("contextConfig.inputStreamWebResource",
            webResource.getWebappPath()), e);
      }
    }
  }


  protected void processAnnotationsUrl(URL url, WebXml fragment,
      boolean handlesTypesOnly, Map<String, JavaClassCacheEntry> javaClassCache) {
    if (url == null) {
      // Nothing to do.
      return;
    } else if ("jar".equals(url.getProtocol()) || url.toString().endsWith(".jar")) {
      processAnnotationsJar(url, fragment, handlesTypesOnly, javaClassCache);
    } else if ("file".equals(url.getProtocol())) {
      try {
        processAnnotationsFile(
            new File(url.toURI()), fragment, handlesTypesOnly, javaClassCache);
      } catch (URISyntaxException e) {
        log.error(sm.getString("contextConfig.fileUrl", url), e);
      }
    } else {
      log.error(sm.getString("contextConfig.unknownUrlProtocol",
          url.getProtocol(), url));
    }

  }


  protected void processAnnotationsJar(URL url, WebXml fragment,
      boolean handlesTypesOnly, Map<String, JavaClassCacheEntry> javaClassCache) {

    try (Jar jar = JarFactory.newInstance(url)) {
      if (log.isDebugEnabled()) {
        log.debug(sm.getString(
            "contextConfig.processAnnotationsJar.debug", url));
      }

      jar.nextEntry();
      String entryName = jar.getEntryName();
      while (entryName != null) {
        if (entryName.endsWith(".class")) {
          try (InputStream is = jar.getEntryInputStream()) {
            processAnnotationsStream(is, fragment, handlesTypesOnly, javaClassCache);
          } catch (IOException e) {
            log.error(sm.getString("contextConfig.inputStreamJar",
                entryName, url), e);
          } catch (ClassFormatException e) {
            log.error(sm.getString("contextConfig.inputStreamJar",
                entryName, url), e);
          }
        }
        jar.nextEntry();
        entryName = jar.getEntryName();
      }
    } catch (IOException e) {
      log.error(sm.getString("contextConfig.jarFile", url), e);
    }
  }


  protected void processAnnotationsFile(File file, WebXml fragment,
      boolean handlesTypesOnly, Map<String, JavaClassCacheEntry> javaClassCache) {

    if (file.isDirectory()) {
      // Returns null if directory is not readable
      String[] dirs = file.list();
      if (dirs != null) {
        if (log.isDebugEnabled()) {
          log.debug(sm.getString(
              "contextConfig.processAnnotationsDir.debug", file));
        }
        for (String dir : dirs) {
          processAnnotationsFile(
              new File(file, dir), fragment, handlesTypesOnly, javaClassCache);
        }
      }
    } else if (file.getName().endsWith(".class") && file.canRead()) {
      try (FileInputStream fis = new FileInputStream(file)) {
        processAnnotationsStream(fis, fragment, handlesTypesOnly, javaClassCache);
      } catch (IOException e) {
        log.error(sm.getString("contextConfig.inputStreamFile",
            file.getAbsolutePath()), e);
      } catch (ClassFormatException e) {
        log.error(sm.getString("contextConfig.inputStreamFile",
            file.getAbsolutePath()), e);
      }
    }
  }


  protected void processAnnotationsStream(InputStream is, WebXml fragment,
      boolean handlesTypesOnly, Map<String, JavaClassCacheEntry> javaClassCache)
      throws ClassFormatException, IOException {

    ClassParser parser = new ClassParser(is);
    JavaClass clazz = parser.parse();
    checkHandlesTypes(clazz, javaClassCache);

    if (handlesTypesOnly) {
      return;
    }

    processClass(fragment, clazz);
  }


  protected void processClass(WebXml fragment, JavaClass clazz) {
    AnnotationEntry[] annotationsEntries = clazz.getAnnotationEntries();
    if (annotationsEntries != null) {
      String className = clazz.getClassName();
      for (AnnotationEntry ae : annotationsEntries) {
        String type = ae.getAnnotationType();
        if ("Ljavax/servlet/annotation/WebServlet;".equals(type)) {
          processAnnotationWebServlet(className, ae, fragment);
        } else if ("Ljavax/servlet/annotation/WebFilter;".equals(type)) {
          processAnnotationWebFilter(className, ae, fragment);
        } else if ("Ljavax/servlet/annotation/WebListener;".equals(type)) {
          fragment.addListener(className);
        } else {
          // Unknown annotation - ignore
        }
      }
    }
  }


  /**
   * For classes packaged with the web application, the class and each super class needs to be
   * checked for a match with {@link HandlesTypes} or for an annotation that matches {@link
   * HandlesTypes}.
   *
   * @param javaClass      the class to check
   * @param javaClassCache a class cache
   */
  protected void checkHandlesTypes(JavaClass javaClass,
      Map<String, JavaClassCacheEntry> javaClassCache) {

    // Skip this if we can
    if (typeInitializerMap.size() == 0) {
      return;
    }

    if ((javaClass.getAccessFlags() &
        org.apache.tomcat.util.bcel.Const.ACC_ANNOTATION) != 0) {
      // Skip annotations.
      return;
    }

    String className = javaClass.getClassName();

    Class<?> clazz = null;
    if (handlesTypesNonAnnotations) {
      // This *might* be match for a HandlesType.
      populateJavaClassCache(className, javaClass, javaClassCache);
      JavaClassCacheEntry entry = javaClassCache.get(className);
      if (entry.getSciSet() == null) {
        try {
          populateSCIsForCacheEntry(entry, javaClassCache);
        } catch (StackOverflowError soe) {
          throw new IllegalStateException(sm.getString(
              "contextConfig.annotationsStackOverflow",
              context.getName(),
              classHierarchyToString(className, entry, javaClassCache)));
        }
      }
      if (!entry.getSciSet().isEmpty()) {
        // Need to try and load the class
        clazz = Introspection.loadClass(context, className);
        if (clazz == null) {
          // Can't load the class so no point continuing
          return;
        }

        for (ServletContainerInitializer sci : entry.getSciSet()) {
          Set<Class<?>> classes = initializerClassMap.get(sci);
          if (classes == null) {
            classes = new HashSet<>();
            initializerClassMap.put(sci, classes);
          }
          classes.add(clazz);
        }
      }
    }

    if (handlesTypesAnnotations) {
      AnnotationEntry[] annotationEntries = javaClass.getAnnotationEntries();
      if (annotationEntries != null) {
        for (Map.Entry<Class<?>, Set<ServletContainerInitializer>> entry :
            typeInitializerMap.entrySet()) {
          if (entry.getKey().isAnnotation()) {
            String entryClassName = entry.getKey().getName();
            for (AnnotationEntry annotationEntry : annotationEntries) {
              if (entryClassName.equals(
                  getClassName(annotationEntry.getAnnotationType()))) {
                if (clazz == null) {
                  clazz = Introspection.loadClass(
                      context, className);
                  if (clazz == null) {
                    // Can't load the class so no point
                    // continuing
                    return;
                  }
                }
                for (ServletContainerInitializer sci : entry.getValue()) {
                  initializerClassMap.get(sci).add(clazz);
                }
                break;
              }
            }
          }
        }
      }
    }
  }


  private String classHierarchyToString(String className,
      JavaClassCacheEntry entry, Map<String, JavaClassCacheEntry> javaClassCache) {
    JavaClassCacheEntry start = entry;
    StringBuilder msg = new StringBuilder(className);
    msg.append("->");

    String parentName = entry.getSuperclassName();
    JavaClassCacheEntry parent = javaClassCache.get(parentName);
    int count = 0;

    while (count < 100 && parent != null && parent != start) {
      msg.append(parentName);
      msg.append("->");

      count++;
      parentName = parent.getSuperclassName();
      parent = javaClassCache.get(parentName);
    }

    msg.append(parentName);

    return msg.toString();
  }

  private void populateJavaClassCache(String className, JavaClass javaClass,
      Map<String, JavaClassCacheEntry> javaClassCache) {
    if (javaClassCache.containsKey(className)) {
      return;
    }

    // Add this class to the cache
    javaClassCache.put(className, new JavaClassCacheEntry(javaClass));

    populateJavaClassCache(javaClass.getSuperclassName(), javaClassCache);

    for (String interfaceName : javaClass.getInterfaceNames()) {
      populateJavaClassCache(interfaceName, javaClassCache);
    }
  }

  private void populateJavaClassCache(String className,
      Map<String, JavaClassCacheEntry> javaClassCache) {
    if (!javaClassCache.containsKey(className)) {
      String name = className.replace('.', '/') + ".class";
      try (InputStream is = context.getLoader().getClassLoader().getResourceAsStream(name)) {
        if (is == null) {
          return;
        }
        ClassParser parser = new ClassParser(is);
        JavaClass clazz = parser.parse();
        populateJavaClassCache(clazz.getClassName(), clazz, javaClassCache);
      } catch (ClassFormatException e) {
        log.debug(sm.getString("contextConfig.invalidSciHandlesTypes",
            className), e);
      } catch (IOException e) {
        log.debug(sm.getString("contextConfig.invalidSciHandlesTypes",
            className), e);
      }
    }
  }

  private void populateSCIsForCacheEntry(JavaClassCacheEntry cacheEntry,
      Map<String, JavaClassCacheEntry> javaClassCache) {
    Set<ServletContainerInitializer> result = new HashSet<>();

    // Super class
    String superClassName = cacheEntry.getSuperclassName();
    JavaClassCacheEntry superClassCacheEntry =
        javaClassCache.get(superClassName);

    // Avoid an infinite loop with java.lang.Object
    if (cacheEntry.equals(superClassCacheEntry)) {
      cacheEntry.setSciSet(EMPTY_SCI_SET);
      return;
    }

    // May be null of the class is not present or could not be loaded.
    if (superClassCacheEntry != null) {
      if (superClassCacheEntry.getSciSet() == null) {
        populateSCIsForCacheEntry(superClassCacheEntry, javaClassCache);
      }
      result.addAll(superClassCacheEntry.getSciSet());
    }
    result.addAll(getSCIsForClass(superClassName));

    // Interfaces
    for (String interfaceName : cacheEntry.getInterfaceNames()) {
      JavaClassCacheEntry interfaceEntry =
          javaClassCache.get(interfaceName);
      // A null could mean that the class not present in application or
      // that there is nothing of interest. Either way, nothing to do here
      // so move along
      if (interfaceEntry != null) {
        if (interfaceEntry.getSciSet() == null) {
          populateSCIsForCacheEntry(interfaceEntry, javaClassCache);
        }
        result.addAll(interfaceEntry.getSciSet());
      }
      result.addAll(getSCIsForClass(interfaceName));
    }

    cacheEntry.setSciSet(result.isEmpty() ? EMPTY_SCI_SET : result);
  }

  private Set<ServletContainerInitializer> getSCIsForClass(String className) {
    for (Map.Entry<Class<?>, Set<ServletContainerInitializer>> entry :
        typeInitializerMap.entrySet()) {
      Class<?> clazz = entry.getKey();
      if (!clazz.isAnnotation()) {
        if (clazz.getName().equals(className)) {
          return entry.getValue();
        }
      }
    }
    return EMPTY_SCI_SET;
  }

  private static final String getClassName(String internalForm) {
    if (!internalForm.startsWith("L")) {
      return internalForm;
    }

    // Assume starts with L, ends with ; and uses / rather than .
    return internalForm.substring(1,
        internalForm.length() - 1).replace('/', '.');
  }

  protected void processAnnotationWebServlet(String className,
      AnnotationEntry ae, WebXml fragment) {
    String servletName = null;
    // must search for name s. Spec Servlet API 3.0 - 8.2.3.3.n.ii page 81
    List<ElementValuePair> evps = ae.getElementValuePairs();
    for (ElementValuePair evp : evps) {
      String name = evp.getNameString();
      if ("name".equals(name)) {
        servletName = evp.getValue().stringifyValue();
        break;
      }
    }
    if (servletName == null) {
      // classname is default servletName as annotation has no name!
      servletName = className;
    }
    ServletDef servletDef = fragment.getServlets().get(servletName);

    boolean isWebXMLservletDef;
    if (servletDef == null) {
      servletDef = new ServletDef();
      servletDef.setServletName(servletName);
      servletDef.setServletClass(className);
      isWebXMLservletDef = false;
    } else {
      isWebXMLservletDef = true;
    }

    boolean urlPatternsSet = false;
    String[] urlPatterns = null;

    // List<ElementValuePair> evps = ae.getElementValuePairs();
    for (ElementValuePair evp : evps) {
      String name = evp.getNameString();
      if ("value".equals(name) || "urlPatterns".equals(name)) {
        if (urlPatternsSet) {
          throw new IllegalArgumentException(sm.getString(
              "contextConfig.urlPatternValue", "WebServlet", className));
        }
        urlPatternsSet = true;
        urlPatterns = processAnnotationsStringArray(evp.getValue());
      } else if ("description".equals(name)) {
        if (servletDef.getDescription() == null) {
          servletDef.setDescription(evp.getValue().stringifyValue());
        }
      } else if ("displayName".equals(name)) {
        if (servletDef.getDisplayName() == null) {
          servletDef.setDisplayName(evp.getValue().stringifyValue());
        }
      } else if ("largeIcon".equals(name)) {
        if (servletDef.getLargeIcon() == null) {
          servletDef.setLargeIcon(evp.getValue().stringifyValue());
        }
      } else if ("smallIcon".equals(name)) {
        if (servletDef.getSmallIcon() == null) {
          servletDef.setSmallIcon(evp.getValue().stringifyValue());
        }
      } else if ("asyncSupported".equals(name)) {
        if (servletDef.getAsyncSupported() == null) {
          servletDef.setAsyncSupported(evp.getValue()
              .stringifyValue());
        }
      } else if ("loadOnStartup".equals(name)) {
        if (servletDef.getLoadOnStartup() == null) {
          servletDef
              .setLoadOnStartup(evp.getValue().stringifyValue());
        }
      } else if ("initParams".equals(name)) {
        Map<String, String> initParams = processAnnotationWebInitParams(evp
            .getValue());
        if (isWebXMLservletDef) {
          Map<String, String> webXMLInitParams = servletDef
              .getParameterMap();
          for (Map.Entry<String, String> entry : initParams
              .entrySet()) {
            if (webXMLInitParams.get(entry.getKey()) == null) {
              servletDef.addInitParameter(entry.getKey(), entry
                  .getValue());
            }
          }
        } else {
          for (Map.Entry<String, String> entry : initParams
              .entrySet()) {
            servletDef.addInitParameter(entry.getKey(), entry
                .getValue());
          }
        }
      }
    }
    if (!isWebXMLservletDef && urlPatterns != null) {
      fragment.addServlet(servletDef);
    }
    if (urlPatterns != null) {
      if (!fragment.getServletMappings().containsValue(servletName)) {
        for (String urlPattern : urlPatterns) {
          fragment.addServletMapping(urlPattern, servletName);
        }
      }
    }

  }

  /**
   * process filter annotation and merge with existing one! FIXME: refactoring method too long and
   * has redundant subroutines with processAnnotationWebServlet!
   *
   * @param className The filter class name
   * @param ae        The filter annotation
   * @param fragment  The corresponding fragment
   */
  protected void processAnnotationWebFilter(String className,
      AnnotationEntry ae, WebXml fragment) {
    String filterName = null;
    // must search for name s. Spec Servlet API 3.0 - 8.2.3.3.n.ii page 81
    List<ElementValuePair> evps = ae.getElementValuePairs();
    for (ElementValuePair evp : evps) {
      String name = evp.getNameString();
      if ("filterName".equals(name)) {
        filterName = evp.getValue().stringifyValue();
        break;
      }
    }
    if (filterName == null) {
      // classname is default filterName as annotation has no name!
      filterName = className;
    }
    FilterDef filterDef = fragment.getFilters().get(filterName);
    FilterMap filterMap = new FilterMap();

    boolean isWebXMLfilterDef;
    if (filterDef == null) {
      filterDef = new FilterDef();
      filterDef.setFilterName(filterName);
      filterDef.setFilterClass(className);
      isWebXMLfilterDef = false;
    } else {
      isWebXMLfilterDef = true;
    }

    boolean urlPatternsSet = false;
    boolean servletNamesSet = false;
    boolean dispatchTypesSet = false;
    String[] urlPatterns = null;

    for (ElementValuePair evp : evps) {
      String name = evp.getNameString();
      if ("value".equals(name) || "urlPatterns".equals(name)) {
        if (urlPatternsSet) {
          throw new IllegalArgumentException(sm.getString(
              "contextConfig.urlPatternValue", "WebFilter", className));
        }
        urlPatterns = processAnnotationsStringArray(evp.getValue());
        urlPatternsSet = urlPatterns.length > 0;
        for (String urlPattern : urlPatterns) {
          // % decoded (if required) using UTF-8
          filterMap.addURLPattern(urlPattern);
        }
      } else if ("servletNames".equals(name)) {
        String[] servletNames = processAnnotationsStringArray(evp
            .getValue());
        servletNamesSet = servletNames.length > 0;
        for (String servletName : servletNames) {
          filterMap.addServletName(servletName);
        }
      } else if ("dispatcherTypes".equals(name)) {
        String[] dispatcherTypes = processAnnotationsStringArray(evp
            .getValue());
        dispatchTypesSet = dispatcherTypes.length > 0;
        for (String dispatcherType : dispatcherTypes) {
          filterMap.setDispatcher(dispatcherType);
        }
      } else if ("description".equals(name)) {
        if (filterDef.getDescription() == null) {
          filterDef.setDescription(evp.getValue().stringifyValue());
        }
      } else if ("displayName".equals(name)) {
        if (filterDef.getDisplayName() == null) {
          filterDef.setDisplayName(evp.getValue().stringifyValue());
        }
      } else if ("largeIcon".equals(name)) {
        if (filterDef.getLargeIcon() == null) {
          filterDef.setLargeIcon(evp.getValue().stringifyValue());
        }
      } else if ("smallIcon".equals(name)) {
        if (filterDef.getSmallIcon() == null) {
          filterDef.setSmallIcon(evp.getValue().stringifyValue());
        }
      } else if ("asyncSupported".equals(name)) {
        if (filterDef.getAsyncSupported() == null) {
          filterDef
              .setAsyncSupported(evp.getValue().stringifyValue());
        }
      } else if ("initParams".equals(name)) {
        Map<String, String> initParams = processAnnotationWebInitParams(evp
            .getValue());
        if (isWebXMLfilterDef) {
          Map<String, String> webXMLInitParams = filterDef
              .getParameterMap();
          for (Map.Entry<String, String> entry : initParams
              .entrySet()) {
            if (webXMLInitParams.get(entry.getKey()) == null) {
              filterDef.addInitParameter(entry.getKey(), entry
                  .getValue());
            }
          }
        } else {
          for (Map.Entry<String, String> entry : initParams
              .entrySet()) {
            filterDef.addInitParameter(entry.getKey(), entry
                .getValue());
          }
        }

      }
    }
    if (!isWebXMLfilterDef) {
      fragment.addFilter(filterDef);
      if (urlPatternsSet || servletNamesSet) {
        filterMap.setFilterName(filterName);
        fragment.addFilterMapping(filterMap);
      }
    }
    if (urlPatternsSet || dispatchTypesSet) {
      Set<FilterMap> fmap = fragment.getFilterMappings();
      FilterMap descMap = null;
      for (FilterMap map : fmap) {
        if (filterName.equals(map.getFilterName())) {
          descMap = map;
          break;
        }
      }
      if (descMap != null) {
        String[] urlsPatterns = descMap.getURLPatterns();
        if (urlPatternsSet
            && (urlsPatterns == null || urlsPatterns.length == 0)) {
          for (String urlPattern : filterMap.getURLPatterns()) {
            // % decoded (if required) using UTF-8
            descMap.addURLPattern(urlPattern);
          }
        }
        String[] dispatcherNames = descMap.getDispatcherNames();
        if (dispatchTypesSet
            && (dispatcherNames == null || dispatcherNames.length == 0)) {
          for (String dis : filterMap.getDispatcherNames()) {
            descMap.setDispatcher(dis);
          }
        }
      }
    }

  }

  protected String[] processAnnotationsStringArray(ElementValue ev) {
    List<String> values = new ArrayList<>();
    if (ev instanceof ArrayElementValue) {
      ElementValue[] arrayValues =
          ((ArrayElementValue) ev).getElementValuesArray();
      for (ElementValue value : arrayValues) {
        values.add(value.stringifyValue());
      }
    } else {
      values.add(ev.stringifyValue());
    }
    String[] result = new String[values.size()];
    return values.toArray(result);
  }

  protected Map<String, String> processAnnotationWebInitParams(
      ElementValue ev) {
    Map<String, String> result = new HashMap<>();
    if (ev instanceof ArrayElementValue) {
      ElementValue[] arrayValues =
          ((ArrayElementValue) ev).getElementValuesArray();
      for (ElementValue value : arrayValues) {
        if (value instanceof AnnotationElementValue) {
          List<ElementValuePair> evps = ((AnnotationElementValue) value)
              .getAnnotationEntry().getElementValuePairs();
          String initParamName = null;
          String initParamValue = null;
          for (ElementValuePair evp : evps) {
            if ("name".equals(evp.getNameString())) {
              initParamName = evp.getValue().stringifyValue();
            } else if ("value".equals(evp.getNameString())) {
              initParamValue = evp.getValue().stringifyValue();
            } else {
              // Ignore
            }
          }
          result.put(initParamName, initParamValue);
        }
      }
    }
    return result;
  }

  private static class DefaultWebXmlCacheEntry {

    private final WebXml webXml;
    private final long globalTimeStamp;
    private final long hostTimeStamp;

    public DefaultWebXmlCacheEntry(WebXml webXml, long globalTimeStamp,
        long hostTimeStamp) {
      this.webXml = webXml;
      this.globalTimeStamp = globalTimeStamp;
      this.hostTimeStamp = hostTimeStamp;
    }

    public WebXml getWebXml() {
      return webXml;
    }

    public long getGlobalTimeStamp() {
      return globalTimeStamp;
    }

    public long getHostTimeStamp() {
      return hostTimeStamp;
    }
  }

  private static class HostWebXmlCacheCleaner implements LifecycleListener {

    @Override
    public void lifecycleEvent(LifecycleEvent event) {

      if (Lifecycle.AFTER_DESTROY_EVENT.equals(event.getType())) {
        Host host = (Host) event.getSource();
        hostWebXmlCache.remove(host);
      }
    }
  }

  static class JavaClassCacheEntry {

    public final String superclassName;

    public final String[] interfaceNames;

    private Set<ServletContainerInitializer> sciSet = null;

    public JavaClassCacheEntry(JavaClass javaClass) {
      superclassName = javaClass.getSuperclassName();
      interfaceNames = javaClass.getInterfaceNames();
    }

    public String getSuperclassName() {
      return superclassName;
    }

    public String[] getInterfaceNames() {
      return interfaceNames;
    }

    public Set<ServletContainerInitializer> getSciSet() {
      return sciSet;
    }

    public void setSciSet(Set<ServletContainerInitializer> sciSet) {
      this.sciSet = sciSet;
    }
  }
}
